package adfc2.render3d.mesh;

import adfc2.render3d.Colors;
import adfc2.render3d.IRenderable;
import adfc2.render3d.TextureReference;
import adfc2.util.Lexer;
import adfc2.util.Parser;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import static adfc2.util.Lexer.TokenType.EOF;
import static adfc2.util.Lexer.TokenType.LONGINT;

/**
 * Загрузчик моделей из текстового представления
 * <p/>
 * Синтаксис v1:
 * <pre>
 * MESHES ::= 'MESHV' '=' version { MESH }
 * MESH ::= name ':' '[' { PRIMITIVE } ']'
 * PRIMITIVE ::= PRIMITIVE_TYPE '[' { PRIMITIVE_PARAM } ']'
 * PRIMITIVE_TYPE ::= 'BOX' | 'CUBE' | 'CYLINDER' | 'ELLIPSOID' | 'SPHERE' | 'FRAME' | 'QUADS'
 * PRIMITIVE_PARAM ::= TEXTURE_DEF | COLOR_DEF | RADIUS1_DEF | RADIUS2_DEF | RADIUS3_DEF | SIZE1_DEF | SIZE2_DEF |
 *                     SIZE3_DEF | HEIGHT_DEF | POSITION_DEF | EXTRA_DEF | VERTICES
 * TEXTURE_DEF ::= texture_constant | TILE_TEXTURE | SIMPLE_TEXTURE
 * TILE_TEXTURE ::= 'TEXTILE' '[' texture_name ',' tile_idx ',' texture_width ',' texture_height ']'
 * SIMPLE_TEXTURE ::= 'TEXTURE' '=' texture_name
 * COLOR_DEF ::= 'C' '=' color_constant | 'C' '=' '(' red ',' green ',' blue [ ',' alpha ] ')' | 'C' '=' hexrgb
 * RADIUS1_DEF ::= 'R' '=' radius
 * RADIUS2_DEF ::= 'R' '=' '(' radius_x ',' radius_y ')'
 * RADIUS3_DEF ::= 'R' '=' '(' radius_x ',' radius_y ',' radius_z ')'
 * SIZE1_DEF ::= 'S' '=' size
 * SIZE2_DEF ::= 'S' '=' '(' size_x ',' size_y ')'
 * SIZE3_DEF ::= 'S' '=' '(' size_x ',' size_y ',' size_z ')'
 * HEIGHT_DEF := 'H' '=' height
 * POSITION_DEF ::= 'P' '=' '(' pos_x ',' pos_y ',' pos_z ')'
 * EXTRA_DEF ::= 'X' '=' extra | 'X' '=' '(' extra_param { ',' extra_param } ')'
 * VERTICES ::= 'V' '=' '(' vertex_x ',' vertex_y ',' vertex_z { ';' vertex_x ',' vertex_y ',' vertex_z } ')'
 * </pre>
 * Параметры зависят от типа примитива:
 * <ul>
 * <li>BOX, CUBE: TEXTURE_DEF, COLOR_DEF, SIZE1_DEF или SIZE3_DEF, POSITION_DEF</li>
 * <li>CYLINDER: TEXTURE_DEF, COLOR_DEF, POSITION_DEF, RADIUS1_DEF или RADIUS2_DEF, HEIGHT_DEF</li>
 * <li>ELLIPSOID, SPHERE: TEXTURE_DEF, COLOR_DEF, POSITION_DEF, RADIUS1_DEF или RADIUS3_DEF</li>
 * <li>FRAME: TEXTURE_DEF, COLOR_DEF, SIZE2_DEF или SIZE3_DEF, POSITION_DEF, EXTRA_DEF (толщина)</li>
 * <li>QUADS: TEXTURE_DEF, COLOR_DEF, VERTICES</li>
 * </ul>
 */
public class MeshLoader extends Parser {

    private class PrimitiveParams {
        TextureReference texture = TextureReference.NO_TEXTURE;
        double[] color = Colors.WHITE;
        double[] radius = {};
        double[] size = {};
        double height;
        double[] position = {0, 0, 0};
        double[] extra = {};
        double[] vertices;
    }

    public MeshLoader(Lexer lexer) {
        super(lexer);
    }

    public Map<String, Mesh> loadMeshes() throws IOException {
        ready();
        expect("MESHESV");
        expect('=');
        int version = getInt();
        Map<String, Mesh> meshes = new HashMap<String, Mesh>();
        switch (version) {
            case 1: {
                while (!check(EOF)) {
                    String name = getId();
                    expect(':');
                    meshes.put(name, v1loadMesh());
                }
            }
            break;
            default:
                throw new IOException("Bad mesh version " + version);
        }
        return meshes;
    }

    public Mesh v1loadMesh() throws IOException {
        ready();
        return v1mesh();
    }

    private Mesh v1mesh() throws IOException {
        // MESH ::= 'MESHV' '=' version '[' { PRIMITIVE } ']'
        expect('[');
        List<IRenderable> parts = new LinkedList<IRenderable>();
        while (!check(']')) {
            parts.add(v1primitive());
        }
        return new Mesh(parts);
    }

    private IRenderable v1primitive() throws IOException {
        // PRIMITIVE ::= PRIMITIVE_TYPE '[' { PRIMITIVE_PARAM } ']'
        // PRIMITIVE_TYPE ::= 'BOX' | 'CUBE' | 'CYLINDER' | 'ELLIPSOID' | 'SPHERE' | 'FRAME' | 'QUADS'
        String type = getId();
        if (type.equals("BOX") || type.equals("CUBE")) {
            return v1box();
        } else if (type.equals("CYLINDER")) {
            return v1cylinder();
        } else if (type.equals("ELLIPSOID") || type.equals("SPHERE")) {
            return v1sphere();
        } else if (type.equals("FRAME")) {
            return v1frame();
        } else if (type.equals("QUADS")) {
            return v1quads();
        } else throw new IOException("Unknown primitive type " + type);
    }

    private SimpleQuads v1quads() throws IOException {
        // QUADS: TEXTURE_DEF, COLOR_DEF, VERTICES
        PrimitiveParams params = v1primitiveParams();
        return new SimpleQuads(params.color, params.vertices, params.texture);
    }

    private IRenderable v1frame() throws IOException {
        // FRAME: TEXTURE_DEF, COLOR_DEF, SIZE2_DEF или SIZE3_DEF, POSITION_DEF, EXTRA_DEF (толщина)
        PrimitiveParams params = v1primitiveParams();
        if (params.extra.length != 1) throw new IOException("Bad frame thickness def");
        if (params.size.length == 2) params.size = new double[]{params.size[0], params.size[1], params.extra[0]};
        else if (params.size.length != 3) throw new IOException("Bad frame params def");
        return new Frame(params.color, params.position, params.size, params.extra[0], params.texture);
    }

    private Ellipsoid v1sphere() throws IOException {
        // ELLIPSOID, SPHERE: TEXTURE_DEF, COLOR_DEF, POSITION_DEF, RADIUS1_DEF или RADIUS3_DEF
        PrimitiveParams params = v1primitiveParams();
        if (params.radius.length == 1)
            params.radius = new double[]{params.radius[0], params.radius[0], params.radius[0]};
        else if (params.radius.length != 3) throw new IOException("Bad ellipsoid radius def");
        return new Ellipsoid(params.color, params.position, params.radius, params.texture);
    }

    private Cylinder v1cylinder() throws IOException {
        // CYLINDER: TEXTURE_DEF, COLOR_DEF, POSITION_DEF, RADIUS1_DEF или RADIUS2_DEF, HEIGHT_DEF<
        PrimitiveParams params = v1primitiveParams();
        double[] rrh;
        if (params.radius.length == 1) rrh = new double[]{params.radius[0], params.radius[0], params.height};
        else if (params.radius.length == 2) rrh = new double[]{params.radius[0], params.radius[1], params.height};
        else throw new IOException("Bad cylinder radius def");
        return new Cylinder(params.color, params.position, rrh, params.texture);
    }

    private Box v1box() throws IOException {
        // BOX, CUBE: TEXTURE_DEF, COLOR_DEF, SIZE1_DEF или SIZE3_DEF, POSITION_DEF
        PrimitiveParams params = v1primitiveParams();
        if (params.size.length == 1) params.size = new double[]{params.size[0], params.size[0], params.size[0]};
        else if (params.size.length != 3) throw new IOException("Bad cube size def");
        return new Box(params.color, params.position, params.size, params.texture);
    }

    private PrimitiveParams v1primitiveParams() throws IOException {
        //  PRIMITIVE_PARAM ::= TEXTURE_DEF | COLOR_DEF | RADIUS1_DEF | RADIUS2_DEF | RADIUS3_DEF | SIZE1_DEF | SIZE2_DEF |
        //                      SIZE3_DEF | HEIGHT_DEF | POSITION_DEF | EXTRA_DEF | VERTICES
        PrimitiveParams params = new PrimitiveParams();
        expect('[');
        while (!check(']')) {
            String param = getId();
            if (param.equals("TEXTURE_NONE")) {
                params.texture = TextureReference.NO_TEXTURE;
            } else if (param.startsWith("TEXREF_")) {
                try {
                    int idx = Integer.parseInt(param.substring("TEXREF_".length()));
                    if (idx < 0 || idx >= TextureReference.TEXREFS.length)
                        throw new IOException("Bad primitive param " + param);
                    params.texture = TextureReference.TEXREFS[idx];
                } catch (NumberFormatException ex) {
                    throw new IOException("Bad primitive param " + param);
                }
            } else if (param.equals("TEXTILE")) {
                // TILE_TEXTURE ::= 'TEXTILE' '[' texture_name ',' tile_idx ']'
                expect('[');
                String texname = getString();
                expect(',');
                int tile_idx = getInt();
                expect(']');
                throw new UnsupportedOperationException("TILE_TEXTURE not supported yet"); // TODO implement TILE_TEXTURE
            } else if (param.equals("TEXTURE")) {
                // SIMPLE_TEXTURE ::= 'TEXTURE' '=' texture_name
            } else if (param.equals("C")) {
                // COLOR_DEF ::= 'C' '=' color_constant | 'C' '=' '(' red ',' green ',' blue [ ',' alpha ] ')' | 'C' '=' hexrgb
                expect('=');
                if (check(LONGINT)) {
                    long hexrgb = (Long) last.param;
                    long r = (hexrgb & 0xff0000) >> 16;
                    long g = (hexrgb & 0x00ff00) >> 8;
                    long b = (hexrgb & 0x0000ff);
                    params.color = new double[]{
                            r / 256.0, g / 256.0, b / 256.0, 1.0
                    };
                } else if (checkString()) {
                    String colorname = (String) last.param;
                    double[] color = Colors.COLOR_CONSTANTS.get(colorname);
                    if (color != null) {
                        params.color = color;
                    } else if (colorname.equals("null")) {
                        params.color = null;
                    } else {
                        throw new IOException("Bad color constant " + last.param);
                    }
                } else {
                    double[] rgb = getDoubleArray();
                    if (rgb.length == 3) {
                        params.color = new double[]{rgb[0], rgb[1], rgb[2], 1.0};
                    } else if (rgb.length == 4) {
                        params.color = rgb;
                    } else throw new IOException("Bad color def");
                }
            } else if (param.equals("R")) {
                // RADIUS1_DEF ::= 'R' '=' radius
                // RADIUS2_DEF ::= 'R' '=' '(' radius_x ',' radius_y ')'
                // RADIUS3_DEF ::= 'R' '=' '(' radius_x ',' radius_y ',' radius_z ')'
                expect('=');
                params.radius = getDoubleOrArray();
            } else if (param.equals("S")) {
                // SIZE1_DEF ::= 'S' '=' size
                // SIZE2_DEF ::= 'S' '=' '(' size_x ',' size_y ')'
                // SIZE3_DEF ::= 'S' '=' '(' size_x ',' size_y ',' size_z ')'
                expect('=');
                params.size = getDoubleOrArray();
            } else if (param.equals("H")) {
                // HEIGHT_DEF := 'H' '=' height
                expect('=');
                params.height = getDouble();
            } else if (param.equals("P")) {
                // POSITION_DEF ::= 'P' '=' '(' pos_x ',' pos_y ',' pos_z ')'
                expect('=');
                params.position = getDoubleArray(3);
            } else if (param.equals("X")) {
                // EXTRA_DEF ::= 'X' '=' extra | 'X' '=' '(' extra_param { ',' extra_param } ')'
                expect('=');
                params.extra = getDoubleOrArray();
            } else if (param.equals("V")) {
                // VERTICES ::= 'V' '=' '(' vertex_x ',' vertex_y ',' vertex_z { ';' vertex_x ',' vertex_y ',' vertex_z } ')'
                expect('=');
                expect('(');
                if (check(')')) {
                    params.vertices = new double[]{};
                } else {
                    List<double[]> vertices = new LinkedList<double[]>();
                    do {
                        double[] vxyz = new double[3];
                        vxyz[0] = getDouble();
                        expect(',');
                        vxyz[1] = getDouble();
                        expect(',');
                        vxyz[2] = getDouble();
                        vertices.add(vxyz);
                    } while (check(';'));
                    expect(')');
                    params.vertices = new double[vertices.size() * 3];
                    int i = 0;
                    for (double[] vxyz : vertices) {
                        System.arraycopy(vxyz, 0, params.vertices, i, 3);
                        i += 3;
                    }
                }
            } else {
                throw new IOException("Bad primitive param " + param);
            }
        }
        return params;
    }

/*
 *     <li></li>
 *     <li>CYLINDER: TEXTURE_DEF, COLOR_DEF, POSITION_DEF, RADIUS1_DEF или RADIUS2_DEF, HEIGHT_DEF</li>
 *     <li>ELLIPSOID, SPHERE: TEXTURE_DEF, COLOR_DEF, POSITION_DEF, RADIUS1_DEF или RADIUS3_DEF</li>
 *     <li>FRAME: TEXTURE_DEF, COLOR_DEF, SIZE2_DEF или SIZE3_DEF, POSITION_DEF, EXTRA_DEF (толщина)</li>
 *     <li>QUADS: TEXTURE_DEF, COLOR_DEF, VERTICES</li>
 */

}
